;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 介绍：	【51单片机汇编】用Keil C51模拟器的UART#1窗口查看串口输出
; NOTE:		File format: UTF-8
; 备注：	1、本工程默认使用Keil模拟器运行，无需硬件；按F7编译后，按Ctrl+F5运行，
;			调出UART #1窗口（要先运行程序）：点击软件的View-->Serial Windows-->UART #1，
;			在Keil软件的UART #1窗口中看串口的输出结果为Hello world!....；
;			2、使用C语言从Keil模拟器输出串口信息的步骤详见我的另外两篇介绍：
;			https://gitee.com/langcai1943/8051-from-boot-to-application#1hello-world%E8%BE%93%E5%87%BA
;			https://gitee.com/langcai1943/8051-from-boot-to-application#5%E7%94%A8%E6%B1%87%E7%BC%96%E4%BB%8Ekeil%E8%B0%83%E8%AF%95%E7%AA%97%E5%8F%A3%E4%B8%AD%E8%BE%93%E5%87%BAhello-world
; 作者		将狼才鲸
; 日期		2023-06-11
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;
; 包含头文件和其它源文件，也可以用#include <REGISTERS51.INC>，同样有效；
; 被包含的文件不要用END指令放在文件尾，否则编译器会报错
$INCLUDE(REGISTERS_51.INC)
$INCLUDE(START.ASM)
$INCLUDE(IRQ.ASM)

	ORG	0100H	; 1、RESET函数从RAM 0x100地址开始，将0xFF之前的RAM留给堆栈，
				; 堆栈之前是中断入口地址；
				; 2、注意：RAM地址（包含中断入口地址）和寄存器地址是分开的，它们地址
				; 重叠但含义不同，并不会相互干扰

	; 标号以冒号:结尾，兼具C语言的标号和函数名的功能；可以跳转到标号处
;;
; 功能：类似于C语言的main()函数，程序主入口
; 参数：无
; 返回：无
;;
RESET:	
	NOP	; 空指令，消耗一个时钟周期的时间，什么也不做
;	MOV IE, #00H	; 屏蔽所有中断；MOV指令类似于C语言的=赋值，IE = 0x00;
	CLR	RS0	; RS1 RS0的值为00时，选择R0~7的通用寄存器为4组中的第0组；
			; 清零一个位，标准C语言里没有直接的位操作，需要PSW &= ~RS0_MASK;（仅展示逻辑）
	CLR	RS1

;	MOV	SP, #0C0H	; 程序运行开始时需要对SP堆栈地址进行设置，一般设置到80H之后，
					; 给堆栈留足空间，但是又不占用中断入口地址
	MOV SP, #3FH	; 当前本工程用的AT89C51芯片默认工程，未设置外部RAM配置，中断占的地址少，
					; 而内部RAM只有0x80大小，所以设置到0x3F

	LCALL UART_INIT	; 1、调用函数，C语言中类似的用法是your_func();
					; 2、LCALL和RET成对使用，跳转前会将当前地址放到堆栈，而LJMP遇到
					; 遇到RET也会返回，但会返回最开始的地址，或者返回到不知道哪里去了；
					; 3、单步执行到LCALL位置的话，想继续进入子函数，需要按F11，不能再按F10，
					; 按F10会一次性执行完子程序，并单步跳过子程序
	LCALL UART_PRINT

LOOP:	; 汇编函数不遇到RET指令的话会一直往后执行，所以RESET这个主程序会进入下面的死循环
	NOP
	LJMP	LOOP
	RET

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
UART_INIT:
	; SCON: D7~D0: SM0 SM1 SM2 REN      TB8 RB8 TI RI
	;              通讯设置    接收使能 数据位9 收发中断
	;MOV SCON, #50H	; 1、1个起始位0，8个数据位和1个停止位1，接收使能；
					; 2、当前这个配置好像不对，还有波特率什么的也没配，直接用这条语句
					; 反而Keil模拟器没有串口输出
					; 3、Keil使用模拟器调试时，不初始化串口也能从串口收发数据，所以屏蔽这条
	RET	; 汇编中函数返回

INFO_PRINT_1:			; 类似于字符串常量宏定义
DB 'Hello world!....'	; DB类似于C语言定义一个常量，当前总共16字节数据
INFO1_LEN EQU 10H		; 类似于#define INFO1_LEN 0x10

;;
; 函数功能：	串口发送字符串数据
; 参数：		无，使用全局变量INFO_PRINT_1
; 返回值：		无
;;
UART_PRINT:
	; 局部变量R0，类似于C语言中for循环中的i；
	; A，类似于地址中的数据buf[i];
	MOV  A, R0		; 将通用寄存器R0存到ACC累加器，累加器调用时可以用名字ACC，可以用名字A，
					; 写程序时常用ACC作一个变量，类似于C语言的int tmp，然后使用tmp
	PUSH ACC		; 1、压栈R0，防止之前别人有用通用寄存器，先压栈报错，别破坏别的
					; 中断或函数已经存过的值；
					; 2、R0~R7相当于C语言定义了int tmp0; ~ int tmp7;七个全局变量，每个函数
					; 都很少使用局部变量，而是使用这些全局变量，因为R0~R7速度快，因为大家都用，
					; 所以函数进入和退出要压栈推栈；你只需要知道哪些会被别人用，和本函数会用
					; 哪些R0~R7之间的全局变量，然后处理对应的R0~R7寄存器即可；
					; 3、C语言的函数执行也有压栈弹栈操作，不过是编译器自动完成的，所以
					; 嵌入式编程时还需要注意堆栈大小，不能在函数内定义超出堆栈长度的数组，
					; 否则程序会跑飞；

	MOV  R0, #00H	; 类似于：int i = 0;
PRINT_LOOP_1:
	MOV  DPTR, #INFO_PRINT_1	; 1、把字符串指针放入DPL DPH数据指针中，DPTR是两个寄存器合在一起的名字
								; 2、不像C语言定义一个char *buf; 可以直接使用buf里的数据，
								; 汇编使用缓存或数组地址时要先放进DPTR，DPTR就类似于buf地址；
								; 3、立即数常量的使用前加#号，这种固定用法和指令集有关
	MOV  A, R0			; for (int A = 0;... 此时将A寄存器作为类似C语言里循环的i变量
	MOVC A, @A + DPTR	; A = *(A + DPTR), 整条语句是一条固定的指令，这里的@是一个固定的用法
						; MOVC是查表指令，也是类似于C语言的=赋值，还有@取址的效果；
						; 类似于C语言的for(...i++) {A = buf[i];}
	LCALL UART_SEND		; 1、从串口发送1个字节，串口收发中断都是一个字节一次，不像有些32位
						; CPU的串口模块，串口中断来时会携带长度信息，一次收多字节数据；
						; 2、传入参数为A: 要发送的一个字节；
	INC  R0			; R0++，类似于C语言中for循环的i++
	CJNE R0, #INFO1_LEN, PRINT_LOOP_1	; 1、比较累加器和立即数,不相等则转移
										; 2、立即数宏定义的使用前加#号，这种固定用法和指令集有关

	POP  ACC		; 弹栈
	MOV  R0, A		; 将压入的值还原
	RET				; 函数返回

;;
; 功能：串口发送1字节数据（非中断模式，是轮询模式）
; 参数：A	1字节数据
; 返回：无
;;
UART_SEND:
	MOV SBUF, A
LB_SEND_WAIT:	; 等待最后一个bit发送完成
	MOV A, SCON
	ANL A, #02H	; 查询发送中断标志，一个字节是否发送完成；A &= (0x1 << 1); 从0算起的第1bit
	JZ  LB_SEND_WAIT	; A为0则跳转，if (A == 0) continue
	ANL SCON, #0FDH		; C语言：SCON &= 0xFD，也就是SCON &= ~(0x1 << 1)，清除发送中断标志
	RET

END
